Vue cli 3.0项目优化总结Vue Router懒加载+按需引入+高可用CDN

前言

使用Vue.js构建的单页面应用(SPA - single page application),需要引入很多的库,包括自家的router、vuex等,第三方的axios、loadash等等,不做任何配置打包之后的文件就有10M+,在第一次载入页面的时候需要加载完整的应用代码,会出现长时间的白屏,用户体验极差。

我们的目标就是减少包体积、提升加载速度,同时保证可用性、维护性、不侵入业务代码。

具体的优化方法包括:

  • Vue Router 懒加载
  • CDN引入依赖
  • 第三方库按需引入
  • 开启Gzip
  • 关闭生产环境sourceMap

诊断与验证

既然我们要做优化,我们必须先知道从哪些方面下手,最后如何去验证我们优化的效果。

  • dist目录大小,最终打包的所有文件大小
  • webpack-bundle-analyzer,分析包中包含的模块的大小
  • 浏览器调试工具,开启Disable Cache,查看客户端加载的资源大小以及速度

主要介绍一下webpack-bundle-analyzer,看官方介绍:

This module will help you:
  1.Realize what’s really inside your bundle
  2.Find out what modules make up the most of its size
  3.Find modules that got there by mistake
  4.Optimize it!

效果图

使用方法

Vue CLI 3 默认支持打包报告,其实就是webpack-bundle-analyzer这个插件,使用vue-cli-service build --report就会在dist目录下生成一个report.html,打开这个页面就可以看到分析报告了。

为了方便使用,我们可以在package.json的scripts下面新增一行:"report": "vue-cli-service build --report",这样就可以使用npm run report命令实现了。

PS:老版本的vue cli可以通过npm run build --report命令,打包完成后会自动打开分析页面

怎么看这个报告呢?其实很简单,每一个分区代表打包以后的一个js文件,不同的颜色代表了文件的大小,从大到小,分区里面会嵌套分区,表示包对应的子模块;左侧的菜单展示了每个js文件在stat(原始)、parsed(编译后)、gizpped(压缩后)三种情况的大小。

优化前后对比

好不好,看疗效。我们先看结果再看过程。

优化前

  • dist目录大小为13.3MB
  • 打包后的本地js文件(/dist/static/js/):
  • 浏览器请求到的js文件:
  • webpack-bundle-analyzer:

    可以看到,实际请求时需要加载两个本地js文件一共1.95M,还有一个第三方js文件815kb,合计2.74M,用时接近10s(吐槽下公司网络)。

优化后

  • dist目录大小为2.95MB
  • 打包后的本地js文件(/dist/static/js/):
  • 浏览器请求到的js文件:
  • webpack-bundle-analyzer:

    可以看到,实际请求时请求了4个本地js文件一共156.5kb,CDN加载了6个包,一共207.5kb,合计364kb,用时2.2s,注意,优化后是包含了CDN资源的大小,网上很多网上都没有计算CDN的资源。对比优化前体积减少了87%,加载时间减少了78%(受网路影响,不太精确)。显而易见,优化效果非常显著,基本实现了秒开页面,用户体验杠杠滴!

优化过程

Vue Router 懒加载

首先看官方文档,是这样描述懒加载的

当打包构建应用时,Javascript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。

结合 Vue 的异步组件和 Webpack 的代码分割功能,轻松实现路由组件的懒加载。

实际操作非常简单,只需要把src/router.js中的组件引入方式从

1
import Page404 from './src/Page404.vue';

变更为

1
const page404 = () => import('./views/404.vue');

就这样,over。当然,你想自定义分块也是可以的,通过 命名 chunk可以将多个组件打包到一个异步块中。

1
2
3
const Foo = () => import(/* webpackChunkName: "group-foo" */ './Foo.vue')
const Bar = () => import(/* webpackChunkName: "group-foo" */ './Bar.vue')
const Baz = () => import(/* webpackChunkName: "group-foo" */ './Baz.vue')

最后的效果就是,打包后的js文件由之前的app.js和chunk-vendors.js多出了很多chunk,浏览器访问时会按需加载对应的chunk,从而减少了首屏的加载时间。

CDN引入依赖

CDN引入依赖很简单,使用script标签引入对应的资源即可,但是存在几个问题:

  1. CDN服务器挂了怎么办?

    要是我们能检测到CDN是否成功就好了,当然能。常用的依赖包都会创建一个全局变量,通过检测这个环节变量就可以知道CDN的资源是否加载成功,不成功就加载服务器资源就可以了。缺点就是服务器上必须存放一份依赖包的备份,会增加包体积,但是并不影响访问速度。

    怎么知道依赖包的全局变量呢?我们用axios举例,打开axios的Github的dist目录,可以看到axios.js和axios.min.js,我们要使用的是min包,但我们要在axios.js中去查找他的全局变量。其中有这样一段代码。

    1
    2
    3
    4
    5
    6
    7
    8
    if(typeof exports === 'object' && typeof module === 'object')
    module.exports = factory();
    else if(typeof define === 'function' && define.amd)
    define([], factory);
    else if(typeof exports === 'object')
    exports["axios"] = factory();
    else
    root["axios"] = factory();

    我们就可以知道axios的全局变量就是axios了,在我们的publick/index.html中就可以这样检测

    1
    2
    3
    <!--引入axios-->
    <script src="https://cdn.jsdelivr.net/npm/axios@0.18.0/dist/axios.min.js"></script>
    <script>window.axios || document.write('<script src="<%= BASE_URL %>js/axios.min.js"><\/script>')</script>

    这样就实现了高可用的CDN引入依赖了,不怕挂!

  2. 引入min包,没有了错误提醒,原有的业务代码中使用import引入的方式,CDN引入后无法使用了,维护两份代码?

    为了减小包体积提升速度,CDN需要引入min包,但是就损失依赖自带的错误提醒,只适用于生产环境,传统的做法就是维护两分代码,开发版使用完整包 或者import引入,生产环境引入min包,这对于一个懒人来说太麻烦了。

    要是能这样就好了,业务代码使用import方式引入依赖,开发环境加载完整包,生产环境使用CDN加载min包,完美!但是,这可能吗?当然可能,使用webpack的externals+html-webpack-plugin就可以轻松搞定啦!

    externals的作用就是在使用CDN的情况下,业务代码中仍然可以使用import引入依赖。官方文档是这样说的

    防止将某些 import 的包(package)打包到 bundle 中,而是在运行时(runtime)再去从外部获取这些扩展依赖(external dependencies)

    externals在vue.config.js中的配置webpack

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    configureWebpack: config => {
    if (process.env.NODE_ENV === 'production') {
    config.externals = {
    'vue': 'Vue',
    'vuex': 'Vuex',
    'vue-router': 'VueRouter',
    'axios': 'axios',
    'vue-grid-layout': 'VueGridLayout'
    };
    }
    }

    配置的格式:import包名: 全局变量,工作原理就是webpack在读取到import语句时会从externals中去查找对应的全局变量然后加载。比如'vue-router': 'VueRouter',业务代码中使用import router from 'vue-router',webpack就会去查找VueRouter这个全局变量。

    通过process.env.NODE_ENV === 'production'判断生产环境才配置externals,开发环境仍然加载npm install安装的依赖,这样就解决了生产环境使用import引入读取CDN资源的问题,但是开发环境CDN资源仍然会加载,我们并不会用到,这种浪费浏览器资源的事情是不能容忍的,必须干掉!这时候就需要html-webpack-plugin登场了。

    html-webpack-plugin在Vue CLI 中主要负责处理public/index.html,具体查看文档,在public/index.html中判断运行环境决定是否启用CDN。

    1
    2
    3
    4
    5
    <% if (htmlWebpackPlugin.options.environment === 'production') { %>
    <!--引入axios-->
    <script src="https://cdn.jsdelivr.net/npm/axios@0.18.0/dist/axios.min.js"></script>
    <script>window.axios || document.write('<script src="<%= BASE_URL %>js/axios.min.js"><\/script>')</script>
    <% } %>

    这个environment并不是自带的,需要我们再vue.config.js中进行配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 在htmlWebpackPlugin中增加环境变量,在index.html中使用
    chainWebpack: config => {
    config
    .plugin('html')
    .tap(args => {
    args[0].environment = process.env.NODE_ENV;
    return args;
    });
    }

    至此,就实现高可用CDN的工程化了,不需要改业务代码,不需要开发环境生产环境两套代码,三分钟解决烦恼!

第三方库按需引入

都有了CDN了,为什么还需要按需引入呢?举个例子,项目中需要使用lodash的deepClone和isEqual两个方法,使用前面的方法引入loadash,虽然有CDN的加持,但是我们仍然需要加载lodash的完整包,为了两个方法就要加载完整包还是有些得不偿失的,这种情况按需引入可以获得更快的加载速度。

具体按需引入的方法,echartselement-ui等官方文档都有说明,照着操作就可以了。

lodash的按需引入稍微麻烦一些,需要安装babel-plugin-lodash这个包

1
npm install babel-plugin-lodash -D

然后配置babel.config.js

1
2
3
4
5
6
7
8
module.exports = {
presets: [
['@babel/env', { 'targets': { 'node': 6 }}]
],
'plugins': [
'lodash'
]
};

引用部分

1
2
3
4
5
6
7
8
import lodash from 'lodash';

const _lodash = {
cloneDeep: lodash.cloneDeep,
isEqual: lodash.isEqual
};

window._ = _lodash;

只有通过lodash的.运算符调用的方法才会被引入

开启Gzip

nginx的Gzip有两种方式,一种是服务器端的Gzip,这个大家都知道,就是每次请求时服务器先压缩再返回资源,对服务器性能有一定消耗;另一种是Gzip_static,就是打包时生成.gz文件,每次请求时服务器直接返回.gz文件,不消耗服务器性能。两种开启一种就可以了。

  1. 服务器端Gzip

    很简单,只需要配置一下nginx的配置就可以了

    1
    2
    3
    4
    5
    6
    7
    8
    # 开启gzip
    gzip on;
    # gzip 压缩级别,1-10,数字越大压缩的越好,也越占用CPU
    gzip_comp_level 5;
    # 启用gzip压缩的最小文件,小于设置值的文件将不会压缩
    gzip_min_length 256;
    # 进行压缩的文件类型。javascript有多种形式。其中的值可以在 mime.types 文件中找到
    gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript;
  2. Gzip_static

    需要安装compression-webpack-plugin实现打包为.gz文件,然后开启nginx的gzip_static让nginx优先查找文件的.gz版本发送给客户端。

    1
    npm install compression-webpack-plugin -D

    vue.config.js中配置webpack

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const productionGzipExtensions = ['js', 'css']
    configureWebpack: {
    plugins: [
    new CompressionWebpackPlugin({
    asset: '[path].gz[query]',
    algorithm: 'gzip',
    test: new RegExp('\\.(' + productionGzipExtensions.join('|') + ')$'),
    threshold: 10240,
    minRatio: 0.8
    })
    ]
    }

    nginx配置

    1
    2
    # 开启gzip_static
    gzip_static on;

其他配置

  1. 配置vue.config.js关闭生成环境的sourcemap

    1
    productionSourceMap: false
  2. 减少不必要的第三方依赖

  3. 安装依赖区分开发模式,只在开发模式使用的依赖使用dev模式安装,例如

    1
    npm install xxx --save-dev

总结

优化是没有极限的,不同的项目需要采用不同的优化方案,没有最好的,只有最合适的。填坑之路很漫长,但是我相信,办法总比困难多。

0%